package qy.jalgotrade.broker.backtesting;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.math.DoubleMath;

import qy.jalgotrade.bar.Bar;
import qy.jalgotrade.bar.Bars;
import qy.jalgotrade.barfeed.BaseBarFeed;
import qy.jalgotrade.broker.Broker;
import qy.jalgotrade.broker.InstrumentTraits;
import qy.jalgotrade.broker.IntegerTraits;
import qy.jalgotrade.broker.LimitOrder;
import qy.jalgotrade.broker.MarketOrder;
import qy.jalgotrade.broker.Order;
import qy.jalgotrade.broker.Order.Action;
import qy.jalgotrade.broker.OrderEvent;
import qy.jalgotrade.broker.OrderExecutionInfo;
import qy.jalgotrade.broker.fillstrategy.DefaultStrategy;
import qy.jalgotrade.broker.fillstrategy.FillInfo;
import qy.jalgotrade.broker.fillstrategy.FillStrategy;
import qy.jalgotrade.event.EventHandler;
import qy.jalgotrade.utils.CommonUtils;
import qy.jalgotrade.broker.StopLimitOrder;
import qy.jalgotrade.broker.StopOrder;

/**
 * Backtesting broker.
 * 
 * @author qy
 *
 */
public class BacktestingBroker extends Broker implements EventHandler {

	public static final String LOGGER_NAME = "qy.jalgotrade.broker.backtesting";

	private double __cash;

	private Commission __commission;

	private final Map<String, Double> __shares;

	private final Map<String, Double> __instrumentPrice;

	private final Map<String, Order> __activeOrders;

	private boolean __useAdjustedValues;

	private FillStrategy __fillStrategy;

	private final Logger __logger;

	private final BaseBarFeed __barFeed;

	private boolean __allowNegativeCash;

	private int __nextOrderId;

	private boolean __started;

	/**
	 * 
	 * @param cash    The initial amount of cash.
	 * @param barFeed The bar feed that will provide the bars.
	 */
	public BacktestingBroker(double cash, BaseBarFeed barFeed) {

		this(cash, barFeed, null);
	}

	/**
	 * 
	 * 
	 * @param cash       The initial amount of cash.
	 * @param barFeed    The bar feed that will provide the bars.
	 * @param commission An object responsible for calculating order commissions.
	 */
	public BacktestingBroker(double cash, BaseBarFeed barFeed, Commission commission) {

		super();

		assert cash >= 0;
		__cash = cash;
		if (commission == null) {
			__commission = new NoCommission();
		} else {
			__commission = commission;
		}
		__shares = new HashMap<>();
		__instrumentPrice = new HashMap<>(); // Used by setShares
		__activeOrders = new HashMap<>();
		__useAdjustedValues = false;
		__fillStrategy = new DefaultStrategy();
		__logger = LoggerFactory.getLogger(LOGGER_NAME);
		// It is VERY important that the broker subscribes to barfeed events before the
		// strategy.
		/*
		 * Broker 订阅 BarFeed (on-new-value) 事件必须早于 Strategy 的 订阅, 即每当 BarFeed 产生新 Bar 时,
		 * 优先用来进行之前周期的 Order (由 Strategy 产生) 处理, 再用来进行新的 Strategy 处理:
		 */
		barFeed.getNewValuesEvent().subscribe(this);
		__barFeed = barFeed;
		__allowNegativeCash = false;
		__nextOrderId = 1;
		__started = false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see qy.jalgotrade.event.EventHandler#handle(java.lang.String, java.util.Map)
	 */
	@Override
	public void handle(String eventName, Map<String, Object> params) throws Exception {

		ZonedDateTime dateTime = (ZonedDateTime) params.get("dateTime");
		Bars bars = (Bars) params.get("values");
		onBars(dateTime, bars);
	}

	/**
	 * 
	 * @return __nextOrderId
	 */
	protected int _getNextOrderId() {

		return __nextOrderId++;
	}

	/**
	 * 
	 * @param bars bars
	 * @param instrument instrument
	 * @return Bar
	 */
	protected Bar _getBar(Bars bars, String instrument) {

		Bar ret = bars.getBar(instrument);
		if (ret != null) {
			ret = __barFeed.getLastBar(instrument);
		}
		return ret;
	}

	/**
	 * 
	 * @return currentDateTime
	 */
	protected ZonedDateTime _getCurrentDateTime() {

		return __barFeed.getCurrentDateTime();
	}

	/**
	 * 
	 * @param order order
	 */
	protected void _registerOrder(Order order) {

		assert !__activeOrders.containsKey(order.getId());
		assert order.getId() != null;
		__activeOrders.put(order.getId(), order);
	}

	/**
	 * 
	 * @param order order
	 */
	protected void _unregisterOrder(Order order) {

		assert __activeOrders.containsKey(order.getId());
		assert order.getId() != null;
		__activeOrders.remove(order.getId());
	}

	/**
	 * 
	 * @param instrument instrument
	 * @return price
	 */
	protected double _getPriceForInstrument(String instrument) {

		double ret = Double.NaN;

		// Try gettting the price from the last bar first.
		Bar lastBar = __barFeed.getLastBar(instrument);
		if (lastBar != null) {
			ret = lastBar.getPrice();
		} else {
			// Try using the instrument price set by setShares if its available.
			ret = __instrumentPrice.get(instrument);
		}
		return ret;
	}

	public Logger getLogger() {

		return __logger;
	}

	/**
	 * 
	 * @return __useAdjustedValues
	 */
	public boolean getUseAdjustedValues() {

		return __useAdjustedValues;
	}

	/**
	 * 
	 * @param useAdjusted useAdjusted
	 * @throws Exception Exception
	 */
	@Deprecated
	public void setUseAdjustedValues(boolean useAdjusted) throws Exception {

		// TODO: Deprecated since v0.15
		if (!__barFeed.barsHaveAdjClose()) {
			throw new Exception("The barfeed doesn't support adjusted close values");
		}
		__useAdjustedValues = useAdjusted;
	}

	/**
	 * 
	 * @param allowNegativeCash allowNegativeCash
	 */
	public void setAllowNegativeCash(boolean allowNegativeCash) {

		__allowNegativeCash = allowNegativeCash;
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#getInstrumentTraits(java.lang.String)
	 */
	@Override
	public InstrumentTraits getInstrumentTraits(String instrument) {

		return new IntegerTraits();
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#getCash(boolean)
	 */
	@Override
	public double getCash(boolean includeShort) {

		double ret = __cash;
		if (!includeShort && __barFeed.getCurrentBars() != null) {
			Bars bars = __barFeed.getCurrentBars();
			for (Entry<String, Double> e : __shares.entrySet()) {
				String instrument = e.getKey();
				double shares = e.getValue();
				if (shares < 0) {
					double instrumentPrice = _getBar(bars, instrument).getPrice();
					ret += instrumentPrice * shares;
				}
			}
		}
		return ret;
	}

	/**
	 * 
	 * @param cash cash
	 */
	public void setCash(double cash) {

		__cash = cash;
	}

	/*
	 * Returns the portfolio value (cash + shares * price).
	 * 
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#getEquity()
	 */
	@Override
	public double getEquity() {

		double ret = getCash();
		for (Entry<String, Double> e : __shares.entrySet()) {
			String instrument = e.getKey();
			double shares = e.getValue();
			double instrumentPrice = _getPriceForInstrument(instrument);
			assert !Double.isNaN(instrumentPrice) : String.format("Price for %s is missing", instrument);
			ret += instrumentPrice * shares;
		}
		return ret;
	}

	/**
	 * Returns the strategy used to calculate order commissions.
	 * 
	 * @return __commission
	 */
	public Commission getCommission() {

		return __commission;
	}

	/**
	 * Sets the strategy to use to calculate order commissions.
	 * 
	 * @param commission An object responsible for calculating order commissions.
	 */
	public void setCommission(Commission commission) {

		__commission = commission;
	}

	/**
	 * Returns the :class:`pyalgotrade.broker.fillstrategy.FillStrategy` currently set.
	 * 
	 * @return __fillStrategy
	 */
	public FillStrategy getFillStrategy() {

		return __fillStrategy;
	}

	/**
	 * Sets the :class:`pyalgotrade.broker.fillstrategy.FillStrategy` to use.
	 * 
	 * @param strategy strategy
	 */
	public void setFillStrategy(FillStrategy strategy) {

		__fillStrategy = strategy;
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#getShares(java.lang.String)
	 */
	@Override
	public double getShares(String instrument) {

		return __shares.getOrDefault(instrument, 0.0);
	}

	/**
	 * Set existing shares before the strategy starts executing.
	 * 
	 * @param instrument Instrument identifier.
	 * @param quantity   The number of shares for the given instrument.
	 * @param price      The price for each share.
	 */
	public void setShares(String instrument, double quantity, double price) {

		assert !__started : "Can't setShares once the strategy started executing";
		__shares.put(instrument, quantity);
		__instrumentPrice.put(instrument, price);
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#getPositions()
	 */
	@Override
	public Map<String, Double> getPositions() {

		return __shares;
	}

	/**
	 * 
	 * @return
	 */
	public List<String> getActiveInstruments() {

		List<String> ret = new ArrayList<>();
		for (Entry<String, Double> e : __shares.entrySet()) {
			String instrument = e.getKey();
			double shares = e.getValue();
			if (!DoubleMath.fuzzyEquals(shares, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
				ret.add(instrument);
			}
		}
		return ret;
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#getActiveOrders(java.lang.String)
	 */
	@Override
	public List<Order> getActiveOrders(String instrument) {

		List<Order> ret = null;
		if (instrument == null) {
			ret = new ArrayList<>(__activeOrders.values());
		} else {
			ret = new ArrayList<>();
			for (Order order : __activeOrders.values()) {
				if (order.getInstrument().equals(instrument)) {
					ret.add(order);
				}
			}
		}
		return ret;
	}

	/**
	 * Tries to commit an order execution.
	 * 
	 * @param order order
	 * @param dateTime dateTime
	 * @param fillInfo fillInfo
	 * @throws Exception Exception
	 */
	public void commitOrderExecution(Order order, ZonedDateTime dateTime, FillInfo fillInfo) throws Exception {

		double price = fillInfo.getPrice();
		double quantity = fillInfo.getQuantity();

		double cost = 0.0;
		double sharesDelta = 0;
		if (order.isBuy()) {
			cost = price * quantity * -1;
			assert cost < 0;
			sharesDelta = quantity;
		} else if (order.isSell()) {
			cost = price * quantity;
			assert cost > 0;
			sharesDelta = -quantity;
		} else { // Unknown action
			assert false;
		}

		double commission = getCommission().calculate(order, price, quantity);
		cost -= commission;
		double resultingCash = getCash() + cost;

		// Check that we're ok on cash after the commission.
		if (resultingCash >= 0 || __allowNegativeCash) {
			/*
			 * Update the order before updating internal state since addExecutionInfo may raise.
			 * addExecutionInfo should switch the order state.
			 */
			OrderExecutionInfo orderExecutionInfo = new OrderExecutionInfo(price, quantity, commission, dateTime);
			order.addExecutionInfo(orderExecutionInfo);

			// Commit the order execution.
			__cash = resultingCash;
			double updatedShares = order.getInstrumentTraits()
			        .roundQuantity(getShares(order.getInstrument()) + sharesDelta);
			if (DoubleMath.fuzzyEquals(updatedShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
				__shares.remove(order.getInstrument());
			} else {
				__shares.put(order.getInstrument(), updatedShares);
			}

			// Let the strategy know that the order was filled.
			__fillStrategy.onOrderFilled(this, order);

			// Notify the order update
			if (order.isFilled()) {
				_unregisterOrder(order);
				notifyOrderEvent(new OrderEvent(order, OrderEvent.Type.FILLED, orderExecutionInfo));
			} else if (order.isPartiallyFilled()) {
				notifyOrderEvent(new OrderEvent(order, OrderEvent.Type.PARTIALLY_FILLED, orderExecutionInfo));
			} else {
				assert false;
			}

		} else {
			__logger.debug("Not enough cash to fill {} order [{}] for {} share/s", order.getInstrument(), order.getId(),
			        order.getRemaining());
		}
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#submitOrder(qy.jalgotrade.broker.Order)
	 */
	@Override
	public void submitOrder(Order order) throws Exception {

		if (order.isInitial()) {
			order.setSubmitted(Integer.toString(_getNextOrderId()), _getCurrentDateTime());
			_registerOrder(order);
			// Switch from INITIAL -> SUBMITTED
			order.switchState(Order.State.SUBMITTED);
			notifyOrderEvent(
			        new OrderEvent(order, OrderEvent.Type.SUBMITTED, new OrderExecutionInfo(_getCurrentDateTime()))); // TODO: which time for new OrderExecutionInfo?
		} else {
			throw new Exception("The order was already processed");
		}
	}

	/*
	 * In order to properly support market-on-close with intraday feeds I'd need to know about different
	 * exchange/market trading hours and support specifying routing an order to a specific exchange/market.
	 * Even if I had all this in place it would be a problem while paper-trading with a live feed since
	 * I can't tell if the next bar will be the last bar of the market session or not.
	 * 
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#createMarketOrder(qy.jalgotrade.broker.Order.Action, java.lang.String, double, boolean)
	 */
	@Override
	public MarketOrder createMarketOrder(Action action, String instrument, double quantity, boolean onClose)
	        throws Exception {

		if (onClose && __barFeed.isIntraday()) {
			throw new Exception("Market-on-close not supported with intraday feeds");
		}

		return new BacktestingMarketOrder(action, instrument, quantity, onClose, getInstrumentTraits(instrument));
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#createLimitOrder(qy.jalgotrade.broker.Order.Action, java.lang.String, double, double)
	 */
	@Override
	public LimitOrder createLimitOrder(Action action, String instrument, double limitPrice, double quantity)
	        throws Exception {

		return new BacktestingLimitOrder(action, instrument, limitPrice, quantity, getInstrumentTraits(instrument));
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#createStopOrder(qy.jalgotrade.broker.Order.Action, java.lang.String, double, double)
	 */
	@Override
	public StopOrder createStopOrder(Action action, String instrument, double stopPrice, double quantity)
	        throws Exception {

		return new BacktestingStopOrder(action, instrument, stopPrice, quantity, getInstrumentTraits(instrument));
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#createStopLimitOrder(qy.jalgotrade.broker.Order.Action, java.lang.String, double, double, double)
	 */
	@Override
	public StopLimitOrder createStopLimitOrder(Action action, String instrument, double stopPrice, double limitPrice,
	        double quantity) throws Exception {

		return new BacktestingStopLimitOrder(action, instrument, stopPrice, limitPrice, quantity,
		        getInstrumentTraits(instrument));
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.broker.Broker#cancelOrder(qy.jalgotrade.broker.Order)
	 */
	@Override
	public void cancelOrder(Order order) throws Exception {

		Order activeOrder = __activeOrders.get(order.getId());
		if (activeOrder == null) {
			throw new Exception("The order is not active anymore");
		}
		if (activeOrder.isFilled()) {
			throw new Exception("Can't cancel order that has already been filled");
		}

		_unregisterOrder(activeOrder);
		activeOrder.switchState(Order.State.CANCELED);
		notifyOrderEvent(new OrderEvent(activeOrder, OrderEvent.Type.CANCELED,
		        new OrderExecutionInfo(order.getSubmitDateTime(), "User requested cancellation"))); // TODO: which time for new OrderExecutionInfo?
	}

	/**
	 * Return True if further processing is needed.
	 * 
	 * @param order order
	 * @param bar bar
	 * @throws Exception Exception
	 */
	private boolean __preProcessOrder(Order order, Bar bar) throws Exception {

		boolean ret = true;

		// For non-GTC orders we need to check if the order has expired.
		if (!order.getGoodTillCanceled()) {
			boolean expired = bar.getDateTime().toLocalDate()
			        .compareTo(((BacktestingOrder) order).getAcceptedDateTime().toLocalDate()) > 0;

			// Cancel the order if it is expired.
			if (expired) {
				ret = false;
				_unregisterOrder(order);
				order.switchState(Order.State.CANCELED);
				notifyOrderEvent(new OrderEvent(order, OrderEvent.Type.CANCELED,
				        new OrderExecutionInfo(bar.getDateTime(), "Expired"))); // TODO: which time for new OrderExecutionInfo?
			}
		}
		return ret;
	}

	/**
	 * 
	 * @param order order
	 * @param bar bar
	 * @throws Exception Exception
	 */
	private void __postProcessOrder(Order order, Bar bar) throws Exception {

		/*
		 * For non-GTC orders and daily (or greater) bars we need to check if orders should expire right now
		 * before waiting for the next bar.
		 */
		if (!order.getGoodTillCanceled()) {
			boolean expired = false;
			if (__barFeed.getFrequency().getValue() >= Bar.Frequency.DAY.getValue()) {
				expired = bar.getDateTime().toLocalDate()
				        .compareTo(((BacktestingOrder) order).getAcceptedDateTime().toLocalDate()) >= 0;
			}

			// Cancel the order if it will expire in the next bar.
			if (expired) {
				_unregisterOrder(order);
				order.switchState(Order.State.CANCELED);
				notifyOrderEvent(new OrderEvent(order, OrderEvent.Type.CANCELED,
				        new OrderExecutionInfo(bar.getDateTime(), "Expired"))); // TODO: which time for new OrderExecutionInfo?
			}
		}
	}

	/**
	 * 
	 * @param order order
	 * @param bar bar
	 * @throws Exception Exception
	 */
	private void __processOrder(Order order, Bar bar) throws Exception {

		if (!__preProcessOrder(order, bar)) {
			return;
		}

		// Double dispatch to the fill strategy using the concrete order type.
		FillInfo fillInfo = ((BacktestingOrder) order).process(this, bar);
		if (fillInfo != null) {
			commitOrderExecution(order, bar.getDateTime(), fillInfo);
		}

		if (order.isActive()) {
			__postProcessOrder(order, bar);
		}
	}

	/**
	 * 
	 * @param order order
	 * @param bars bars
	 * @throws Exception Exception
	 */
	private void __onBarsImpl(Order order, Bars bars) throws Exception {

		/*
		 * IF WE'RE DEALING WITH MULTIPLE INSTRUMENTS WE SKIP ORDER PROCESSING IF THERE
		 * IS NO BAR FOR THE ORDER'S INSTRUMENT TO GET THE SAME BEHAVIOUR AS IF WERE BE
		 * PROCESSING ONLY ONE INSTRUMENT.
		 */
		Bar bar = bars.getBar(order.getInstrument());
		if (bar != null) {
			// Switch from SUBMITTED -> ACCEPTED
			if (order.isSubmitted()) {
				((BacktestingOrder) order).setAcceptedDateTime(bar.getDateTime());
				order.switchState(Order.State.ACCEPTED);
				notifyOrderEvent(
				        new OrderEvent(order, OrderEvent.Type.ACCEPTED, new OrderExecutionInfo(bar.getDateTime()))); // TODO: which time for new OrderExecutionInfo?
			}

			if (order.isActive()) {
				// This may trigger orders to be added/removed from __activeOrders.
				__processOrder(order, bar);
			} else {
				/*
				 * If an order is not active it should be because it was canceled in this same
				 * loop and it should have been removed.
				 */
				assert order.isCanceled();
				assert !__activeOrders.containsValue(order);
			}
		}
	}

	/**
	 * 
	 * @param dateTime dateTime
	 * @param bars bars
	 * @throws Exception Exception
	 */
	public void onBars(ZonedDateTime dateTime, Bars bars) throws Exception {

		// Let the fill strategy know that new bars are being processed.
		__fillStrategy.onBars(this, bars);
		/*
		 * This is to froze the orders that will be processed in this event, to avoid
		 * new getting orders introduced and processed on this very same event.
		 */
		List<Order> ordersToProcess = new ArrayList<>(__activeOrders.values());

		for (Order order : ordersToProcess) {
			// This may trigger orders to be added/removed from __activeOrders.
			__onBarsImpl(order, bars);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#start()
	 */
	@Override
	public void start() {

		__started = true;
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#stop()
	 */
	@Override
	public void stop() {

	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#join()
	 */
	@Override
	public void join() {

	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#eof()
	 */
	@Override
	public boolean eof() {

		/*
		 * If there are no more events in the barfeed, then there is nothing left for us to do since all processing took
		 * place while processing barfeed events.
		 */
		return __barFeed.eof();
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#preDispatch()
	 */
	@Override
	public void preDispatch() throws Exception {

	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#dispatch()
	 */
	@Override
	public boolean dispatch() throws Exception {

		// All events were already emitted while handling barfeed events.
		return false; // XXX:
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#peekDateTime()
	 */
	@Override
	public ZonedDateTime peekDateTime() {

		return null;
	}
}
